﻿/*
 * Copyright 2009 (c) Alec McEachran, alec@alecmce.com.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
package com.alecmce.preloader
{
	import br.com.stimuli.loading.BulkLoader;
	import br.com.stimuli.loading.BulkProgressEvent;

	import flash.display.Loader;
	import flash.display.MovieClip;
	import flash.display.Sprite;
	import flash.events.ErrorEvent;
	import flash.events.Event;

	/**
	 * This is the document class for a preloader which should be built using
	 * FDT or the MXMLC compiler.
	 * 
	 * The idea behind the preloader is that it will first load XML which
	 * defines an asset that acts as a preloader animation. Once that is
	 * loaded, all other defined assets are loaded and the progress is reflected
	 * by the preloader animation asset. Once all assets have been loaded, the
	 * initial asset, also defined in the XML is added to the stage, and optionally
	 * a method in that asset is triggered.
	 * 
	 * Their are two premise behind this approach: firstly to manage loading a large
	 * number of different initial assets when an application is launched, and secondly
	 * to separate assets from application logic.
	 * 
	 * Projects often have multiple assets that are required over the life of the
	 * application, and that these should all be loaded 'as one' while the preloader
	 * reflects their progress. The list of the assets to be preloaded can be easily
	 * managed by modifying the XML.
	 * 
	 * Modification of the XML can be done independently from the modification of the
	 * preloader animation asset.
	 * 
	 * The structure of the XML is as follows:
	 * 
	 * <config init="mainSwfAsset.swf" method="init">
	 * 
	 *    <preloader url="preloaderAnimationAsset.swf" />
	 *    
	 *    <asset url="mainSwfAsset.swf" />
	 *    <asset url="secondSwfAsset.swf" />
	 *    <asset url="secondPngAsset.png" />
	 *    <asset url="secondPngAsset.png" />
	 *    ...
	 *    <asset url="lastAsset.swf" />
	 * </config>
	 * 
	 * In this example, first the preloaderAnimationAsset.swf will be loaded and
	 * its first frame will be loaded. Then all the <asset ... /> elements will
	 * be loaded and the overall proportion of the loading progress will be
	 * displayed by the frame being updated. When all assets are loaded,
	 * the asset defined in the <config /> tag's init property will added to the
	 * stage, and if it has a method that matches the <config /> tag's method
	 * property, that method will be called without any parameters.
	 * 
	 * The assets will be accessibly by retrieving the preloader singleton:
	 * 
	 * Preloader.assetLoader()
	 *
	 * @author Alec McEachran
	 */
	public class Preloader extends Sprite
	{
		
		/*********************************************************************/
		// Public Static Methods
		/*********************************************************************/
		
		
		/** preloader singleton instance */
		private static var instance:Preloader;
		
		
		/**
		 * @return The bulk loader which contains references to all preloaded assets
		 */
		public static function assetLoader():BulkLoader
		{
			if (!instance)
				return null;
			
			return instance.loader;
		}

		
		/*********************************************************************/
		// Constants
		/*********************************************************************/
		
		
		/** the name of the loader */
		private const NAME:String = "preloader";
		
		/** the default config XML */
		private const DEFAULT_CONFIG:String = "config.xml";
		
		/** the error message that is received if the configuration xml can't be found */
		private const CONFIG_NOT_FOUND:String = "The configuration XML can't be found";
		
		/** the error message that is received if the configuration xml is malformed */
		private const CONFIG_MALFORMED:String = "The configuration XML is malformed";
		
		/** the error message that is received if the preloader SWF can't be found */
		private const PRELOADER_NOT_FOUND:String = "The preloader SWF can't be found";
		
		/** the error message that is received if an asset SWF can't be found */
		private const ASSET_NOT_FOUND:String = "The asset SWF can't be found";
		
		/** the error message that is received if no target asset SWF is specified */
		private const NO_TARGET:String = "The target asset isn't specified or can't be found";
		
		/** the error message that is received if the target asset SWF is specified */
		private const NO_TARGET_METHOD:String = "The target asset doesn't have the init method specified";
		
		
		/***************************************************************/
		// Member Variables
		/***************************************************************/
		
		
		/** the configuration URL */
		private var config:String;
		
		/** the configuration XML */
		private var xml:XML;
		
		/** the bulk loader which is used to load all items */
		private var loader:BulkLoader;
		
		/** the preloader MovieClip */
		private var preloader:MovieClip;
				
		/** the method in the asset to be called */
		public var method:String;
		
		
		/***************************************************************/
		// Public Methods
		/***************************************************************/
		
		
		/**
		 * Class Constructor
		 */
		public function Preloader()
		{
			instance = this;
			
			config = loaderInfo.parameters.config;
			if (!config)
				config = DEFAULT_CONFIG;
			
			loadConfig();
		}
		
		
		/***************************************************************/
		// Config Methods
		/***************************************************************/
		
		
		/**
		 * Load the configuration XML.
		 */
		private function loadConfig():void
		{
			loader = new BulkLoader(NAME);
			loader.addEventListener(ErrorEvent.ERROR, onConfigError, false, 0, true);			loader.addEventListener(Event.COMPLETE, onConfigLoaded, false, 0, true);
			loader.add(config);
			loader.start();
		}
		
		
		/**
		 * Remove bulk loader listeners added while loading the configuration XML.
		 */
		private function removeConfigListeners():void
		{
			loader.removeEventListener(ErrorEvent.ERROR, onConfigError);
			loader.removeEventListener(Event.COMPLETE, onConfigLoaded);
		}
		
		
		/**
		 * Throw an error if the configuration XML cannot be found.
		 * 
		 * @param event An event indicating that the configuration XML can't be
		 * found
		 */
		private function onConfigError(event:ErrorEvent):void
		{
			removeConfigListeners();
			throw new Error(CONFIG_NOT_FOUND);
		}
		
		
		/**
		 * Parse the configuration XML and load the preloader animation asset.
		 * If the XML is malformed throw an error to indicate that fact.
		 * 
		 * @param event The event indicating that the configuration XML has
		 * been successfully loaded
		 */
		private function onConfigLoaded(event:Event):void
		{
			removeConfigListeners();
			
			try
			{
				xml = loader.getXML(config, true);
				
				var url:String = xml.preloader.@url;
				if (url != "")
					loadPreloader(url);
			}
			catch (err:Error)
			{
				throw new Error(CONFIG_MALFORMED);
				return;
			}
		}
		
		
		/*********************************************************************/
		// Preloader Methods
		/*********************************************************************/
		
		
		/**
		 * Load the preloader animation asset.
		 * 
		 * @param url The URL of the preloader animation asset, passed from
		 * the configuration XML
		 */
		private function loadPreloader(url:String):void
		{
			loader.addEventListener(ErrorEvent.ERROR, onPreloaderError, false, 0, true);
			loader.addEventListener(Event.COMPLETE, onPreloaderLoaded, false, 0, true);
			loader.add(url);
			loader.start();
		}
		
		
		/**
		 * Remove the listeners added for the loading of the preloader
		 * animation assset.
		 */
		private function removePreloaderListeners():void
		{
			loader.removeEventListener(ErrorEvent.ERROR, onPreloaderError);
			loader.removeEventListener(Event.COMPLETE, onPreloaderLoaded);
		}
		
		
		/**
		 * Throw an error if the defined preloader animation asset cannot be
		 * found.
		 * 
		 * @param event An event indicating that the preloader asset can't be
		 * found
		 */
		private function onPreloaderError(event:ErrorEvent):void
		{
			removePreloaderListeners();
			throw new Error(PRELOADER_NOT_FOUND);
		}
		
		
		/**
		 * Add the preloader asset to the stage and load all the other assets.
		 * 
		 * @param event The event which indicates that the preloader animation
		 * asset has been loaded
		 */
		private function onPreloaderLoaded(event:Event):void
		{
			removePreloaderListeners();
			
			addPreloader();
			
			try
			{
				loadAssets();
			}
			catch (err:Error)
			{
				throw new Error(CONFIG_MALFORMED);
				return;
			}
		}
		
		
		/**
		 * Add the preloader asset to the stage.
		 */
		private function addPreloader():void
		{
			preloader = loader.getContent(xml.preloader.@url) as MovieClip;
			
			var itemLoader:Loader = preloader.loaderInfo.loader;
			itemLoader.name = NAME;
			
			addChild(itemLoader);
			preloader.gotoAndStop(1);
		}
		
		
		/**
		 * Remove the preloader from the stage (and from memory).
		 */
		private function removePreloader():void
		{
			var itemLoader:Loader = getChildByName(NAME) as Loader;
			removeChild(itemLoader);
			
			loader.remove(xml.preloader.@url);
		}
		
		
		/*********************************************************************/
		// Asset Loader Methods
		/*********************************************************************/
		
		
		/**
		 * Load the assets defined in the configuration XML.
		 */
		private function loadAssets():void
		{
			var node:XML;
			var url:String;
			
			for each (node in xml.asset)
			{
				url = node.@url;
				if (url != "")
					loader.add(url);
			}
			
			loader.addEventListener(ErrorEvent.ERROR, onAssetsError, false, 0, true);
			loader.addEventListener(Event.COMPLETE, onAssetsLoaded, false, 0, true);			loader.addEventListener(BulkProgressEvent.PROGRESS, onAssetsProgress, false, 0, true);
			loader.start();
		}
		
		
		/**
		 * Remove the event listeners that were added for monitoring the
		 * loading of assets.
		 */
		private function removeAssetListeners():void
		{
			loader.removeEventListener(ErrorEvent.ERROR, onAssetsError);
			loader.removeEventListener(Event.COMPLETE, onAssetsLoaded);
			loader.removeEventListener(BulkProgressEvent.PROGRESS, onAssetsProgress);
			
		}
		
		
		/**
		 * Throw an error if an asset cannot be found.
		 * 
		 * @param event An event indicating that an asset can't be found
		 */
		private function onAssetsError(event:ErrorEvent):void
		{
			removeAssetListeners();
			throw new Error(ASSET_NOT_FOUND);
		}
		
		
		/**
		 * Remove the preloader and launch the target asset indicated in
		 * the configuration XML.
		 * 
		 * @param event An event which indicates that all of the assets
		 * have been loaded
		 */
		private function onAssetsLoaded(event:Event):void
		{
			removeAssetListeners();
			
			try
			{
				removePreloader();
				launchTarget();
			}
			catch (err:Error)
			{
				throw new Error(CONFIG_MALFORMED);
				return;
			}
		}
		
		
		/**
		 * Launch the asset defined as the primary asset by the configuration
		 * XML, calling a method in the asset if the method is defined in XML
		 * and exists in the asset.
		 */
		private function launchTarget():void
		{
			var init:String = xml.@init;
			var method:String = xml.@method;
				
			if (!init)
			{
				throw new Error(NO_TARGET);
				return;
			}
			
			var target:MovieClip = loader.getContent(init) as MovieClip;
			var targetLoader:Loader = target.loaderInfo.loader;
			
			addChild(targetLoader);
			
			if (method && target.hasOwnProperty(method))
			{
				var fn:Function = target[method] as Function;
				if (fn == null)
				{
					throw new Error(NO_TARGET_METHOD);
					return;
				}
				
				fn.call();
			}
		}
		
		
		/**
		 * Update the frame of the preloader animation asset in order to
		 * indicate the proportion of the preloading assets that have
		 * been loaded so far.
		 * 
		 * @param event An event from the bulk loader which contains
		 * statistics for how much of the assets have been loaded so far
		 */
		private function onAssetsProgress(event:BulkProgressEvent):void
		{
			var frame:int = (event.percentLoaded * preloader.totalFrames) >> 0;
			preloader.gotoAndStop(frame);
		}
		
	}
}